package io.netty.client.core.heart;

import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;

/**
 * 心跳控制器——链路保活.
 *
 * <li>在链路空闲写超时事件回调中向链路对端主动发送心跳包，实现链路保活.
 * <li>空闲写超时：链路出方向上闲置时长.
 * <li>配合{@link io.netty.handler.timeout.IdleStateHandler}实现.
 * <li>连续n次发送失败，则认为链路失效，抛出{@link HeartBeatFailedException}异常到后置handler进行channel.close处理.
 *
 * <p>{@code new IdleStateHandler(0, time, 0, TimeUnit.MILLISECONDS)}
 * <p>
 * TODO ping-pong机制协议服务端配套支持.
 */
@Slf4j
public class HeartBeatHandler extends ChannelDuplexHandler {

    private final int maxRetry;

    private int retry;

    private String ping;

    public HeartBeatHandler(int retry, String ping) {
        this.maxRetry = retry;
        this.ping = ping;

        resetRetry();
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            final IdleStateEvent stateEvent = (IdleStateEvent) evt;

            if (stateEvent.state() == IdleState.WRITER_IDLE) { //写空闲事件
                log.debug("channel[{}]---> ping...", ctx.channel());
                ctx.channel().writeAndFlush(ping)
                        .addListener(future -> {
                            if (future.isSuccess()) {
                                log.debug("心跳发送成功！链路：{}", ctx.channel());
                                resetRetry();
                            } else {
                                log.warn("心跳发送异常！链路：{}", ctx.channel(), future.cause());
                                if (--retry <= 0) {
                                    ctx.fireExceptionCaught(new HeartBeatFailedException("链路异常：心跳重试次数耗尽仍发送失败！", future.cause())); //进入统一异常处理
                                }
                            }
                        });

                return;
            }
        }

        ctx.fireUserEventTriggered(evt);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        resetRetry();
        ctx.fireChannelRead(msg);
    }

    private void resetRetry() {
        retry = maxRetry;
    }

}
